// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language.Extensions;

internal class PreallocatedAttributeTargetExtension : IPreallocatedAttributeTargetExtension
{
    public string TagHelperAttributeTypeName { get; set; } = "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute";

    public string EncodedHtmlStringTypeName { get; set; } = "Microsoft.AspNetCore.Html.HtmlString";

    public string ExecutionContextVariableName { get; set; } = "__tagHelperExecutionContext";

    public string ExecutionContextAddHtmlAttributeMethodName { get; set; } = "AddHtmlAttribute";

    public string ExecutionContextAddTagHelperAttributeMethodName { get; set; } = "AddTagHelperAttribute";

    public string FormatInvalidIndexerAssignmentMethodName { get; set; } = "InvalidTagHelperIndexerAssignment";

    public void WriteTagHelperHtmlAttributeValue(CodeRenderingContext context, PreallocatedTagHelperHtmlAttributeValueIntermediateNode node)
    {
        context.CodeWriter
            .Write("private static readonly global::")
            .Write(TagHelperAttributeTypeName)
            .Write(" ")
            .Write(node.VariableName)
            .Write(" = ")
            .WriteStartNewObject("global::" + TagHelperAttributeTypeName)
            .WriteStringLiteral(node.AttributeName);

        if (node.AttributeStructure == AttributeStructure.Minimized)
        {
            context.CodeWriter.WriteEndMethodInvocation();
        }
        else
        {
            context.CodeWriter
                .WriteParameterSeparator()
                .WriteStartNewObject("global::" + EncodedHtmlStringTypeName)
                .WriteStringLiteral(node.Value)
                .WriteEndMethodInvocation(endLine: false)
                .WriteParameterSeparator()
                .Write($"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.AttributeStructure}")
                .WriteEndMethodInvocation();
        }
    }

    public void WriteTagHelperHtmlAttribute(CodeRenderingContext context, PreallocatedTagHelperHtmlAttributeIntermediateNode node)
    {
        if (context.Parent as TagHelperIntermediateNode == null)
        {
            var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode));
            throw new InvalidOperationException(message);
        }

        context.CodeWriter
            .WriteStartInstanceMethodInvocation(ExecutionContextVariableName, ExecutionContextAddHtmlAttributeMethodName)
            .Write(node.VariableName)
            .WriteEndMethodInvocation();
    }

    public void WriteTagHelperPropertyValue(CodeRenderingContext context, PreallocatedTagHelperPropertyValueIntermediateNode node)
    {
        context.CodeWriter
            .Write("private static readonly global::")
            .Write(TagHelperAttributeTypeName)
            .Write(" ")
            .Write(node.VariableName)
            .Write(" = ")
            .WriteStartNewObject("global::" + TagHelperAttributeTypeName)
            .WriteStringLiteral(node.AttributeName)
            .WriteParameterSeparator()
            .WriteStringLiteral(node.Value)
            .WriteParameterSeparator()
            .Write($"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.AttributeStructure}")
            .WriteEndMethodInvocation();
    }

    public void WriteTagHelperProperty(CodeRenderingContext context, PreallocatedTagHelperPropertyIntermediateNode node)
    {
        var tagHelperNode = context.Parent as TagHelperIntermediateNode;
        if (tagHelperNode == null)
        {
            var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode));
            throw new InvalidOperationException(message);
        }

        // Ensure that the property we're trying to set has initialized its dictionary bound properties.
        if (node.IsIndexerNameMatch &&
            object.ReferenceEquals(FindFirstUseOfIndexer(tagHelperNode, node), node))
        {
            // Throw a reasonable Exception at runtime if the dictionary property is null.
            context.CodeWriter
                .Write("if (")
                .Write(node.FieldName)
                .Write(".")
                .Write(node.PropertyName)
                .WriteLine(" == null)");
            using (context.CodeWriter.BuildScope())
            {
                // System is in Host.NamespaceImports for all MVC scenarios. No need to generate FullName
                // of InvalidOperationException type.
                context.CodeWriter
                    .Write("throw ")
                    .WriteStartNewObject(nameof(InvalidOperationException))
                    .WriteStartMethodInvocation(FormatInvalidIndexerAssignmentMethodName)
                    .WriteStringLiteral(node.AttributeName)
                    .WriteParameterSeparator()
                    .WriteStringLiteral(node.TagHelper.GetTypeName())
                    .WriteParameterSeparator()
                    .WriteStringLiteral(node.PropertyName)
                    .WriteEndMethodInvocation(endLine: false)   // End of method call
                    .WriteEndMethodInvocation();   // End of new expression / throw statement
            }
        }

        context.CodeWriter
            .WriteStartAssignment(GetPropertyAccessor(node))
            .Write("(string)")
            .Write($"{node.VariableName}.Value")
            .WriteLine(";")
            .WriteStartInstanceMethodInvocation(ExecutionContextVariableName, ExecutionContextAddTagHelperAttributeMethodName)
            .Write(node.VariableName)
            .WriteEndMethodInvocation();
    }

    private static PreallocatedTagHelperPropertyIntermediateNode FindFirstUseOfIndexer(
        TagHelperIntermediateNode tagHelperNode,
        PreallocatedTagHelperPropertyIntermediateNode propertyNode)
    {
        Debug.Assert(tagHelperNode.Children.Contains(propertyNode));
        Debug.Assert(propertyNode.IsIndexerNameMatch);

        for (var i = 0; i < tagHelperNode.Children.Count; i++)
        {
            if (tagHelperNode.Children[i] is PreallocatedTagHelperPropertyIntermediateNode otherPropertyNode &&
                otherPropertyNode.TagHelper.Equals(propertyNode.TagHelper) &&
                otherPropertyNode.BoundAttribute.Equals(propertyNode.BoundAttribute) &&
                otherPropertyNode.IsIndexerNameMatch)
            {
                return otherPropertyNode;
            }
        }

        // This is unreachable, we should find 'propertyNode' in the list of children.
        throw new InvalidOperationException();
    }

    private static string GetPropertyAccessor(PreallocatedTagHelperPropertyIntermediateNode node)
    {
        var propertyAccessor = $"{node.FieldName}.{node.PropertyName}";

        if (node.IsIndexerNameMatch)
        {
            var dictionaryKey = node.AttributeName.Substring(node.BoundAttribute.IndexerNamePrefix.Length);
            propertyAccessor += $"[\"{dictionaryKey}\"]";
        }

        return propertyAccessor;
    }
}
